﻿// --------------------------------------------------------------------------------------------------------------------
// <copyright file="FeedService.cs" company="Open Trader">
//   Copyright (c) David Denis (david.denis@systemathics.com)
// </copyright>
// <summary>
//   |  Open Trader - The Open Source Systematic Trading Platform
//   |
//   |  This program is free software: you can redistribute it and/or modify
//   |  it under the terms of the GNU General Public License as published by
//   |  the Free Software Foundation, either version 2 of the License, or
//   |  (at your option) any later version.
//   |
//   |  This program is distributed in the hope that it will be useful,
//   |  but WITHOUT ANY WARRANTY; without even the implied warranty of
//   |  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
//   |  GNU General Public License for more details.
//   |
//   |  You should have received a copy of the GNU General Public License
//   |  along with this program.  If not, see http://www.gnu.org/licenses
//   |
//   |  Up to date informations about Open Trader can be found at :
//   |    http://opentrader.org
//   |    http://opentrader.codeplex.com
//   |
//   |  For professional services, please visit us at :
//   |    http://www.systemathics.com
// </summary>
// --------------------------------------------------------------------------------------------------------------------

namespace Org.OpenTrader.Framework.Forge
{
    #region Using Directives

    using System;
    using System.Collections.Generic;

    using Org.OpenTrader.Framework.Enums;
    using Org.OpenTrader.Framework.Events;
    using Org.OpenTrader.Framework.Forge.DataCache;
    using Org.OpenTrader.Framework.Forge.Events;
    using Org.OpenTrader.Framework.Forge.Interfaces;
    using Org.OpenTrader.Framework.LiveObjects;
    using Org.OpenTrader.Framework.LiveObjects.Interfaces;

    #endregion

    /// <summary>
    /// The feed service.
    /// </summary>
    [Credits("Feed Service", "Handles all feed operations", "David Denis", "Copyright (C) David Denis <mailto:david.denis@systemathics.com>")]
    [ImplementsInterface("Org.OpenTrader.Framework.Forge.Interfaces.IFeedService")]
    public sealed class FeedService : Service, IFeedService
    {
        private static readonly Identity Identity = Identity.Create(System.Reflection.MethodBase.GetCurrentMethod());

        #region Constants and Fields

        /// <summary>
        /// The subscriptionCells.
        /// </summary>
        private readonly IDictionary<IWiring, SubscriptionCell> subscriptionCells = new Dictionary<IWiring, SubscriptionCell>();

        #endregion

        #region Constructors and Destructors

        /// <summary>
        /// Initializes a new instance of the <see cref="FeedService"/> class.
        /// </summary>
        /// <param name="descriptor">
        /// The descriptor.
        /// </param>
        /// <param name="container">
        /// The container.
        /// </param>
        public FeedService(ILiveObjectDescriptor descriptor, ILiveObjectContainer container)
            : base(descriptor, container)
        {
        }

        #endregion

        #region Properties

        /// <summary>
        /// Gets or sets Wires.
        /// </summary>
        public IWire[] Wires { get; set; }

        #endregion

        #region Implemented Interfaces

        #region IFeedProvider

        /// <summary>
        /// The subscribe.
        /// </summary>
        /// <param name="instrumentID">
        /// The instrumentID.
        /// </param>
        /// <param name="handler">
        /// The handler.
        /// </param>
        /// <returns>
        /// The subscribe.
        /// </returns>
        public Status Subscribe(Guid instrumentID, EventHandler<FeedEventArgs> handler)
        {
            // Related instrument
            var i = Singleton<DC>.Instance.GetInstrumentStaticDataFromCache<IInstrumentStaticData>(instrumentID);
            if (i == null)
            {
                const string Message = "Subscribe: Instrument was not found";
                Logging.Log(Identity, ELogLevel.Error, () => Message);
                return Status.Failure(Identity, Message);
            }

            // No wiring
            if (i.FeedWiring == null)
            {
                const string Message = "Subscribe: Instrument was found but no wiring is available";
                Logging.Log(Identity, ELogLevel.Error, () => Message);
                return Status.Failure(Identity, Message);
            }

            var feedPlugin = this[i.FeedWiring.PluginID] as IFeedPlugin;
            if (feedPlugin == null)
            {
                const string Message = "Subscribe: Instrument and wiring were found but plugin is missing";
                Logging.Log(Identity, ELogLevel.Error, () => Message);
                return Status.Failure(Identity, Message);
            }

            // A subscription cell is used to keep subscription states. 
            // There is one cell per wiring (not per instrument)
            // Per wiring is important because 2 instruments may have equal wirings and thus
            // We may subscribe 2 times to the same instrument on external side, which is not wanted
            SubscriptionCell cell;
            if (!this.subscriptionCells.ContainsKey(i.FeedWiring))
            {
                Logging.Log(Identity, ELogLevel.Debug, () => string.Format("Subscribe: Creating a new subscription cell for wiring {0}", i.FeedWiring));

                // No subscription cell for this wiring, create and add it
                cell = new SubscriptionCell(new EventDispatcher<FeedEventArgs>(i.FeedWiring.ToString()), feedPlugin, i);
                this.subscriptionCells[i.FeedWiring] = cell;
            }
            else
            {
                Logging.Log(Identity, ELogLevel.Debug, () => string.Format("Subscribe: Using previously created subscription cell for wiring {0}", i.FeedWiring));

                // This wiring is already subscribed
                cell = this.subscriptionCells[i.FeedWiring];
            }

            // At this point, the cell as an 1-1 input side (to the plugin) and an active subscription on this input side
            // The cell can have a 1 to n outputs, this is the role of dispatcher (to service users)
            cell.Dispatcher.Subscribe(i.FeedWiring, handler);

            // All done
            const string Msg = "Subscribe: Success";
            Logging.Log(Identity, ELogLevel.Debug, () => Msg);
            return Status.Success(Identity, Msg);
        }

        /// <summary>
        /// The unsubscribe.
        /// </summary>
        /// <param name="instrumentID">
        /// The instrumentID.
        /// </param>
        /// <param name="handler">
        /// The handler.
        /// </param>
        /// <returns>
        /// The unsubscribe.
        /// </returns>
        public Status Unsubscribe(Guid instrumentID, EventHandler<FeedEventArgs> handler)
        {
            // Related instrument
            var i = Singleton<DC>.Instance.GetInstrumentStaticDataFromCache<IInstrumentStaticData>(instrumentID);
            if (i == null)
            {
                const string Message = "Unsubscribe: Instrument was not found";
                Logging.Log(Identity, ELogLevel.Error, () => Message);
                return Status.Failure(Identity, Message);
            }

            // No wiring
            if (i.FeedWiring == null)
            {
                const string Message = "Unsubscribe: Instrument was found but no wiring is available";
                Logging.Log(Identity, ELogLevel.Error, () => Message);
                return Status.Failure(Identity, Message);
            }

            var feedPlugin = this[i.FeedWiring.PluginID] as IFeedPlugin;
            if (feedPlugin == null)
            {
                const string Message = "Unsubscribe: Instrument and wiring were found but plugin is missing";
                Logging.Log(Identity, ELogLevel.Error, () => Message);
                return Status.Failure(Identity, Message);
            }

            // Remove the given handler from dispatcher, eventually it may lead to no more handlers in the dispatcher
            this.subscriptionCells[i.FeedWiring].Dispatcher.Unsubscribe(i.FeedWiring, handler);

            const string Msg = "Unsubscribe: Success";
            Logging.Log(Identity, ELogLevel.Debug, () => Msg);
            return Status.Success(Identity, Msg);
        }

        #endregion

        #region ILiveObject

        /// <summary>
        /// The on activation context.
        /// </summary>
        /// <param name="sender">
        /// The sender.
        /// </param>
        /// <param name="activationEventArgs">
        /// The activation Context.
        /// </param>
        public override void OnActivation(object sender, ActivationEventArgs activationEventArgs)
        {
            if (activationEventArgs is ActivationEventArgs.CreateDestroy)
            {
                switch (activationEventArgs.Type)
                {
                    case EActivationType.Activation:
                        this.Wires = new IWire[this.Plugins.Length];
                        var n = 0;
                        foreach (var v in this.Plugins)
                        {
                            object plugindbg = v;
                            var feedPlugin = v as IFeedPlugin;
                            if (feedPlugin != null)
                            {
                                feedPlugin.ConnectionEventCB += this.OnConnectionEvent;

                                // Wires
                                var wire = new Wire();
                                wire.PluginID = v.Descriptor.ID;
                                wire.PluginName = v.Descriptor.Name;
                                wire.ObjectConfigurationSchema =
                                    this.Container.GetConfigurationSchema(
                                        (feedPlugin.Descriptor as LiveObjectDescriptor.Local).ClassType, feedPlugin.SubscriptionConfigurationSchema);
                                this.Wires[n++] = wire;
                            }
                            else
                            {
                                // TODO : log
                            }
                        }

                        break;

                    case EActivationType.Desactivation:
                        foreach (var v in this.Plugins)
                        {
                            var feedPlugin = v as IFeedPlugin;
                            if (feedPlugin != null)
                            {
                                feedPlugin.ConnectionEventCB -= this.OnConnectionEvent;
                            }
                            else
                            {
                                // TODO : log
                            }
                        }

                        this.Wires = null;
                        this.subscriptionCells.Clear();
                        break;
                }
            }
        }

        #endregion

        #endregion

        #region Methods

        /// <summary>
        /// Track Connection events
        /// </summary>
        /// <param name="sender">
        /// </param>
        /// <param name="cargs">
        /// </param>
        private void OnConnectionEvent(object sender, ConnectionEventArgs cargs)
        {
            if (cargs.Status == EConnectionStatus.Connecting)
            {
                // The Connection is starting (user initiated)
            }
            else if (cargs.Status == EConnectionStatus.Connected)
            {
                // The Connection was started (user initiated)
            }

            if (cargs.Status == EConnectionStatus.Disconnecting)
            {
                // The Connection is going down (user initiated)
            }
            else if (cargs.Status == EConnectionStatus.Disconnected)
            {
                // The Connection went down (user initiated or because of an error)
            }
        }

        #endregion

        /// <summary>
        /// The subscription cell.
        /// </summary>
        internal class SubscriptionCell : IDisposable
        {
            private static readonly Identity Identity = Identity.Create(System.Reflection.MethodBase.GetCurrentMethod());

            #region Constants and Fields

            /// <summary>
            /// The input.
            /// </summary>
            private readonly RemotableEventHandler<FeedEventArgs> input;

            /// <summary>
            /// The book.
            /// </summary>
            private IMarketBook book;

            /// <summary>
            /// The book builder.
            /// </summary>
            private IMarketBookBuilder bookBuilder;

            /// <summary>
            /// The connection status.
            /// </summary>
            private EConnectionStatus connectionStatus;

            #endregion

            #region Constructors and Destructors

            /// <summary>
            /// Initializes a new instance of the <see cref="SubscriptionCell"/> class.
            /// </summary>
            /// <param name="dispatcher">
            /// The dispatcher.
            /// </param>
            /// <param name="plugin">
            /// The plugin.
            /// </param>
            /// <param name="i">
            /// The i.
            /// </param>
            public SubscriptionCell(EventDispatcher<FeedEventArgs> dispatcher, IFeedPlugin plugin, IInstrumentStaticData i)
            {
                this.connectionStatus = plugin.ConnectionStatus;
                this.Instrument = i;
                this.FeedPlugin = plugin;
                this.FeedPlugin.ConnectionEventCB += this.OnConnectionEvent;
                this.input = new RemotableEventHandler<FeedEventArgs>("Input");
                this.input.Subscribe(this.OnFeed);
                this.Dispatcher = dispatcher;
                this.Dispatcher.SubscriptionEventCB += this.OnSubscriptionEvent;

                this.Initialize();
            }

            #endregion

            #region Properties

            /// <summary>
            /// Gets Book.
            /// </summary>
            public IMarketBook Book
            {
                get
                {
                    switch (this.FeedPlugin.EmitedEventsType)
                    {
                        case EEmitedEventsType.BookEvents:
                            return this.book;

                        case EEmitedEventsType.QuoteEvents:
                            return this.bookBuilder.Book;
                    }

                    return null;
                }
            }

            /// <summary>
            /// Gets Dispatcher.
            /// </summary>
            public EventDispatcher<FeedEventArgs> Dispatcher { get; private set; }

            /// <summary>
            /// Gets FeedPlugin.
            /// </summary>
            public IFeedPlugin FeedPlugin { get; private set; }

            /// <summary>
            /// Gets FieldCache.
            /// </summary>
            public IMarketFieldCache FieldCache { get; private set; }

            /// <summary>
            /// Gets InstrumentStaticData.
            /// </summary>
            public IInstrumentStaticData Instrument { get; private set; }

            #endregion

            #region Implemented Interfaces

            #region IDisposable

            /// <summary>
            /// The dispose.
            /// </summary>
            public void Dispose()
            {
                this.Dispatcher.SubscriptionEventCB -= this.OnSubscriptionEvent;
                this.FeedPlugin.ConnectionEventCB -= this.OnConnectionEvent;
            }

            #endregion

            #endregion

            #region Methods

            /// <summary>
            /// The initialize.
            /// </summary>
            private void Initialize()
            {
                switch (this.FeedPlugin.EmitedEventsType)
                {
                    case EEmitedEventsType.BookEvents:
                        Logging.Log(Identity, ELogLevel.Debug, () => "Initialize: This subscription cell will use a regular market book");
                        this.book = new MarketBook(5);
                        break;

                    case EEmitedEventsType.QuoteEvents:
                        Logging.Log(
                            Identity, 
                            ELogLevel.Debug, 
                            () => "Initialize: This subscription cell will use a book builder to handle quotes and produce book events transparently");
                        this.bookBuilder = new MarketBookBuilder(5);
                        break;
                }

                this.FieldCache = new MarketFieldCache();
            }

            /// <summary>
            /// The on Connection event.
            /// </summary>
            /// <param name="sender">
            /// The sender.
            /// </param>
            /// <param name="args">
            /// The args.
            /// </param>
            private void OnConnectionEvent(object sender, ConnectionEventArgs args)
            {
                // Handle correctly the disconnection or connection
                switch (args.Status)
                {
                    case EConnectionStatus.Disconnected:
                        Logging.Log(
                            Identity, 
                            ELogLevel.Debug, 
                            () => string.Format("OnConnectionEvent: Underlying plugin {0} is now disconnected. Starting over", this.FeedPlugin.IdentityString));
                        this.Initialize();
                        break;

                    case EConnectionStatus.Connected:
                        Logging.Log(
                            Identity, 
                            ELogLevel.Debug, 
                            () => string.Format("OnConnectionEvent: Underlying plugin {0} is now connected", this.FeedPlugin.IdentityString));
                        if (this.Dispatcher.Handler.SubscribersCount > 0 && this.FeedPlugin.NeedResubscriptionIfDisconnected)
                        {
                            Logging.Log(
                                Identity, 
                                ELogLevel.Debug, 
                                () =>
                                string.Format(
                                    "OnConnectionEvent: Underlying plugin {0} requires to subscribe on (re)connection. Sending empty snapshots first", 
                                    this.FeedPlugin.IdentityString));
                            this.Dispatcher.DispatchEvent(
                                this.FeedPlugin, new FeedEventArgs.MarketBookUpdate(this.FeedPlugin.Descriptor.ID, new MarketBookCell[] { }, true));
                            this.Dispatcher.DispatchEvent(
                                this.FeedPlugin, new FeedEventArgs.MarketFieldUpdate(this.FeedPlugin.Descriptor.ID, new MarketField[] { }, true));

                            Logging.Log(
                                Identity, 
                                ELogLevel.Debug, 
                                () =>
                                string.Format(
                                    "OnConnectionEvent: Underlying plugin {0} requires to subscribe on (re)connection. Now (re)subscribing {1}", 
                                    this.FeedPlugin.IdentityString, 
                                    this.Instrument.FeedWiring));

                            var result = this.FeedPlugin.StartSubscription(this.Instrument.FeedWiring, this.input);
                            if (!result)
                            {
                                Logging.Log(
                                    Identity, 
                                    ELogLevel.Warn, 
                                    () => string.Format("OnConnectionEvent: Unable to start subscription with wiring {0}", this.Instrument.FeedWiring));
                            }
                            else
                            {
                                Logging.Log(
                                    Identity, 
                                    ELogLevel.Debug, 
                                    () => string.Format("OnConnectionEvent: Started subscription with wiring {0}", this.Instrument.FeedWiring));
                            }
                        }

                        break;
                }

                // Forward the connection event to the dispatcher, encapsulating it in FeedEventArgs.ConnectionStatus
                this.Dispatcher.DispatchEvent(this.FeedPlugin, new FeedEventArgs.ConnectionStatus(args));
            }

            /// <summary>
            /// This is were data coming from plugin is received. Each update type is handled appropriately and then forwarded to dispatcher
            /// - MarketBookCells are placed inside the local book, then forwarded to dispatcher (local book is useful to send snapshots generated locally, see OnSubscriptionEvent)
            /// - MarketQuotes are inserted into the book builder wivhi transform them into MarketQuotes, the latter are then forwarded to dispatcher
            /// - MarketFieldCells are placed inside the local cache, then forwarded to dispatcher
            /// </summary>
            /// <param name="sender">
            /// </param>
            /// <param name="args">
            /// </param>
            private void OnFeed(object sender, FeedEventArgs args)
            {
                if (args is FeedEventArgs.MarketQuoteUpdate)
                {
                    // MarketQuotes require an additional step, they must be sent to the book builder, which will return MarketBookCells afterwards
                    // An unique quote can change many book depths (book shift) or only one, or none if the modification occurs below the actual depth
                    var qargs = args as FeedEventArgs.MarketQuoteUpdate;
                    var update = this.bookBuilder.HandleUpdate(qargs);

                    // Dispatch the set of MarketBookCell produced by the arrival of a MarketQuote
                    this.Dispatcher.DispatchEvent(this.FeedPlugin, update);
                }

                if (args is FeedEventArgs.MarketBookUpdate)
                {
                    // MarketBookCells are used as is
                    var uargs = args as FeedEventArgs.MarketBookUpdate;
                    this.book.HandleUpdate(uargs);

                    // Dispatch MarketBookCell as is
                    this.Dispatcher.DispatchEvent(this.FeedPlugin, args);
                }
                else if (args is FeedEventArgs.MarketFieldUpdate)
                {
                    // MarketFieldCells are used as is
                    var fargs = args as FeedEventArgs.MarketFieldUpdate;
                    this.FieldCache.HandleUpdate(fargs);

                    // Dispatch MarketField as is
                    this.Dispatcher.DispatchEvent(this.FeedPlugin, args);
                }
            }

            /// <summary>
            /// This callback is called when a new handler is added or removed on the multiplexed output side (the service users)
            /// - Add: If the callback is the first to be added, effectively start subscription on the associated plugin and add the handler
            /// - Add: If the callback is not the first, just simulate a snapshot from the local book and add the handler
            /// - Remove: IF the callback is the last to re removed, effectively stop the subscription on the associated plugin
            /// </summary>
            /// <param name="sender">
            /// The sender.
            /// </param>
            /// <param name="args">
            /// The args.
            /// </param>
            private void OnSubscriptionEvent(object sender, SubscriptionEventArgs<FeedEventArgs> args)
            {
                if (args.SubscriptionEventType == ESubscriptionEventType.Subscribing && args.DispatchHandler.SubscribersCount == 0)
                {
                    // We're going to be the first subscriber -> Open the subscriptionif possible
                    if (this.connectionStatus == EConnectionStatus.Connected)
                    {
                        // We're connected, let's start subscription
                        var result = this.FeedPlugin.StartSubscription(args.Wiring as IFeedWiring, this.input);
                        if (!result)
                        {
                            Logging.Log(
                                Identity, ELogLevel.Warn, () => string.Format("OnSubscriptionEvent: Unable to start subscription with wiring {0}", args.Wiring));
                        }
                        else
                        {
                            Logging.Log(Identity, ELogLevel.Debug, () => string.Format("OnSubscriptionEvent: Started subscription with wiring {0}", args.Wiring));
                        }
                    }
                    else if (this.connectionStatus == EConnectionStatus.Disconnected)
                    {
                        // We're not connected, send a disconnected event to inform the using side 
                        // that while the subscription succeeded on service side, the connection is closed 
                        // an thus no feed will come unless connection is opened
                        args.ForwardToSubscriberArgs = new FeedEventArgs[]
                            {
                                new FeedEventArgs.ConnectionStatus(new ConnectionEventArgs(EConnectionStatus.Disconnected)) 
                            };
                    }
                }
                else if (args.SubscriptionEventType == ESubscriptionEventType.Unsubscribing && args.DispatchHandler.SubscribersCount == 1)
                {
                    // We're going to be removed as the last subscriber -> Close the subscription
                    var result = this.FeedPlugin.StopSubscription(args.Wiring as IFeedWiring, this.input);
                    if (! result)
                    {
                        Logging.Log(Identity, ELogLevel.Warn, () => string.Format("OnSubscriptionEvent: Unable to stop subscription with wiring {0}", args.Wiring));
                    }
                    else
                    {
                        Logging.Log(Identity, ELogLevel.Debug, () => string.Format("OnSubscriptionEvent: Stopped subscription with wiring {0}", args.Wiring));
                    }
                }
                else if (args.SubscriptionEventType == ESubscriptionEventType.Subscribing && args.DispatchHandler.SubscribersCount >= 1)
                {
                    // we're another subscriber, joining the game, we must forward all the snapshots before real time events
                    args.ForwardToSubscriberArgs = new FeedEventArgs[] { this.Book.GetSnapshot(), this.FieldCache.GetSnapshot() };
                }
            }

            #endregion
        }
    }
}